﻿// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// Microsoft Public License (MS-PL)
// 
// This license governs use of the accompanying software. If you use the
// software, you accept this license. If you do not accept the license, do not
// use the software.
// 
// 1. Definitions
// 
//   The terms "reproduce," "reproduction," "derivative works," and
//   "distribution" have the same meaning here as under U.S. copyright law. A
//   "contribution" is the original software, or any additions or changes to
//   the software. A "contributor" is any person that distributes its
//   contribution under this license. "Licensed patents" are a contributor's
//   patent claims that read directly on its contribution.
// 
// 2. Grant of Rights
// 
//   (A) Copyright Grant- Subject to the terms of this license, including the
//       license conditions and limitations in section 3, each contributor
//       grants you a non-exclusive, worldwide, royalty-free copyright license
//       to reproduce its contribution, prepare derivative works of its
//       contribution, and distribute its contribution or any derivative works
//       that you create.
// 
//   (B) Patent Grant- Subject to the terms of this license, including the
//       license conditions and limitations in section 3, each contributor
//       grants you a non-exclusive, worldwide, royalty-free license under its
//       licensed patents to make, have made, use, sell, offer for sale,
//       import, and/or otherwise dispose of its contribution in the software
//       or derivative works of the contribution in the software.
// 
// 3. Conditions and Limitations
// 
//   (A) No Trademark License- This license does not grant you rights to use
//       any contributors' name, logo, or trademarks.
// 
//   (B) If you bring a patent claim against any contributor over patents that
//       you claim are infringed by the software, your patent license from such
//       contributor to the software ends automatically.
// 
//   (C) If you distribute any portion of the software, you must retain all
//       copyright, patent, trademark, and attribution notices that are present
//       in the software.
// 
//   (D) If you distribute any portion of the software in source code form, you
//       may do so only under this license by including a complete copy of this
//       license with your distribution. If you distribute any portion of the
//       software in compiled or object code form, you may only do so under a
//       license that complies with this license.
// 
//   (E) The software is licensed "as-is." You bear the risk of using it. The
//       contributors give no express warranties, guarantees or conditions. You
//       may have additional consumer rights under your local laws which this
//       license cannot change. To the extent permitted under your local laws,
//       the contributors exclude the implied warranties of merchantability,
//       fitness for a particular purpose and non-infringement.
//       

namespace Microsoft.ClearScript
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Dynamic;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Runtime.InteropServices.Expando;

    using Microsoft.ClearScript.Util;

    internal partial class HostItem : DynamicObject, IExpando, IDynamic, IScriptMarshalWrapper
    {
        #region Static Fields

        private static readonly MemberMap<Field> fieldMap = new MemberMap<Field>();

        private static readonly MemberMap<Method> methodMap = new MemberMap<Method>();

        private static readonly MemberMap<Property> propertyMap = new MemberMap<Property>();

        #endregion

        #region Fields

        private readonly ScriptEngine engine;

        private readonly HostItemFlags flags;

        private readonly HostTarget target;

        private readonly IDynamic thisDynamic;

        private readonly IExpando thisExpando;

        private Type accessContext;

        private ExtensionMethodSummary cachedExtensionMethodSummary;

        private string[] cachedFieldNames;

        private FieldInfo[] cachedFields;

        private int cachedListCount;

        private string[] cachedLocalEventNames;

        private string[] cachedLocalFieldNames;

        private string[] cachedLocalMethodNames;

        private string[] cachedLocalPropertyNames;

        private string[] cachedMemberNames;

        private string[] cachedMethodNames;

        private MethodInfo[] cachedMethods;

        private PropertyInfo[] cachedProperties;

        private int[] cachedPropertyIndices;

        private string[] cachedPropertyNames;

        private HashSet<string> expandoMemberNames;

        private Dictionary<string, HostIndexedProperty> hostIndexedPropertyMap;

        private Dictionary<string, HostMethod> hostMethodMap;

        private IDynamic targetDynamic;

        private DynamicMetaObject targetDynamicMetaObject;

        private IList targetList;

        private IPropertyBag targetPropertyBag;

        #endregion

        #region Constructors and Destructors

        private HostItem(ScriptEngine engine, HostTarget target, HostItemFlags flags)
        {
            this.engine = engine;
            this.target = target;
            this.flags = flags;

            this.thisExpando = this;
            this.thisDynamic = this;

            this.Initialize();
        }

        #endregion

        #region Public Properties

        public ScriptEngine Engine
        {
            get
            {
                return this.engine;
            }
        }

        public HostItemFlags Flags
        {
            get
            {
                return this.flags;
            }
        }

        public HostTarget Target
        {
            get
            {
                return this.target;
            }
        }

        #endregion

        #region Explicit Interface Properties

        Type IReflect.UnderlyingSystemType
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        #endregion

        #region Public Methods and Operators

        public static object Wrap(ScriptEngine engine, object obj)
        {
            return Wrap(engine, obj, null, HostItemFlags.None);
        }

        public static object Wrap(ScriptEngine engine, object obj, Type type)
        {
            return Wrap(engine, obj, type, HostItemFlags.None);
        }

        public static object Wrap(ScriptEngine engine, object obj, HostItemFlags flags)
        {
            return Wrap(engine, obj, null, flags);
        }

        public object InvokeMember(
            string name, BindingFlags invokeFlags, object[] args, object[] bindArgs, CultureInfo culture)
        {
            this.AdjustInvokeFlags(ref invokeFlags);

            object result;
            if (this.target.TryInvokeAuxMember(name, invokeFlags, args, bindArgs, out result))
            {
                if (this.target is IHostVariable)
                {
                    // the variable may have been reassigned
                    this.BindSpecialTarget();
                }

                return result;
            }

            if (this.targetDynamic != null)
            {
                return this.InvokeDynamicMember(name, invokeFlags, args);
            }

            if (this.targetPropertyBag != null)
            {
                return this.InvokePropertyBagMember(name, invokeFlags, args, bindArgs);
            }

            if (this.targetList != null)
            {
                int index;
                if (int.TryParse(name, NumberStyles.Integer, CultureInfo.InvariantCulture, out index))
                {
                    return this.InvokeListElement(index, invokeFlags, args, bindArgs);
                }
            }

            return this.InvokeHostMember(name, invokeFlags, args, bindArgs, culture);
        }

        public override string ToString()
        {
            return MiscHelpers.FormatInvariant("[{0}]", this.target);
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if ((this.target is HostObject) || (this.target is IHostVariable) || (this.target is IByRefArg))
            {
                if (binder.Type.IsAssignableFrom(this.target.Type))
                {
                    result = Convert.ChangeType(this.target.InvokeTarget, binder.Type);
                    return true;
                }
            }

            result = null;
            return false;
        }

        public override bool TryCreateInstance(CreateInstanceBinder binder, object[] args, out object result)
        {
            result = this.thisDynamic.Invoke(args, true).ToDynamicResult(this.engine);
            return true;
        }

        public override bool TryGetIndex(GetIndexBinder binder, object[] indices, out object result)
        {
            if (indices.Length == 1)
            {
                int index;
                if (MiscHelpers.TryGetIndex(indices[0], out index))
                {
                    result = this.thisDynamic.GetProperty(index).ToDynamicResult(this.engine);
                }
                else
                {
                    result = this.thisDynamic.GetProperty(indices[0].ToString()).ToDynamicResult(this.engine);
                }

                return true;
            }

            throw new InvalidOperationException("Invalid argument or index count");
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = this.thisDynamic.GetProperty(binder.Name).ToDynamicResult(this.engine);
            return true;
        }

        public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
        {
            result = this.thisDynamic.Invoke(args, false).ToDynamicResult(this.engine);
            return true;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            result = this.thisDynamic.InvokeMethod(binder.Name, args).ToDynamicResult(this.engine);
            return true;
        }

        public override bool TrySetIndex(SetIndexBinder binder, object[] indices, object value)
        {
            if (indices.Length == 1)
            {
                int index;
                if (MiscHelpers.TryGetIndex(indices[0], out index))
                {
                    this.thisDynamic.SetProperty(index, value);
                }
                else
                {
                    this.thisDynamic.SetProperty(indices[0].ToString(), value);
                }

                return true;
            }

            throw new InvalidOperationException("Invalid argument or index count");
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            this.thisDynamic.SetProperty(binder.Name, value);
            return true;
        }

        public object Unwrap()
        {
            return this.target.Target;
        }

        #endregion

        #region Explicit Interface Methods

        bool IDynamic.DeleteProperty(string name)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        if (this.targetDynamic != null)
                        {
                            return this.targetDynamic.DeleteProperty(name);
                        }

                        if (this.targetPropertyBag != null)
                        {
                            return this.targetPropertyBag.Remove(name);
                        }

                        if (this.targetDynamicMetaObject != null)
                        {
                            bool result;
                            if (this.targetDynamicMetaObject.TryDeleteMember(name, out result))
                            {
                                return result;
                            }

                            throw new InvalidOperationException("Invalid dynamic member deletion");
                        }

                        throw new NotSupportedException("Object does not support dynamic members");
                    });
        }

        bool IDynamic.DeleteProperty(int index)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        if (this.targetDynamic != null)
                        {
                            return this.targetDynamic.DeleteProperty(index);
                        }

                        if (this.targetPropertyBag != null)
                        {
                            return this.targetPropertyBag.Remove(index.ToString(CultureInfo.InvariantCulture));
                        }

                        if (this.targetDynamicMetaObject != null)
                        {
                            bool result;
                            if (
                                this.targetDynamicMetaObject.TryDeleteMember(
                                    index.ToString(CultureInfo.InvariantCulture), out result))
                            {
                                return result;
                            }

                            throw new InvalidOperationException("Invalid dynamic member deletion");
                        }

                        return false;
                    });
        }

        object IDynamic.GetProperty(string name)
        {
            return this.thisExpando.InvokeMember(
                name,
                BindingFlags.GetProperty,
                null,
                this.thisExpando,
                MiscHelpers.GetEmptyArray<object>(),
                null,
                CultureInfo.InvariantCulture,
                null);
        }

        object IDynamic.GetProperty(int index)
        {
            return this.thisDynamic.GetProperty(index.ToString(CultureInfo.InvariantCulture));
        }

        int[] IDynamic.GetPropertyIndices()
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        bool updated;
                        this.UpdatePropertyNames(out updated);
                        if (updated || (this.cachedPropertyIndices == null))
                        {
                            this.cachedPropertyIndices = this.cachedPropertyNames.GetIndices().Distinct().ToArray();
                        }

                        return this.cachedPropertyIndices;
                    });
        }

        string[] IDynamic.GetPropertyNames()
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        bool updatedFieldNames;
                        this.UpdateFieldNames(out updatedFieldNames);

                        bool updatedMethodNames;
                        this.UpdateMethodNames(out updatedMethodNames);

                        bool updatedPropertyNames;
                        this.UpdatePropertyNames(out updatedPropertyNames);

                        if (updatedFieldNames || updatedMethodNames || updatedPropertyNames
                            || (this.cachedMemberNames == null))
                        {
                            this.cachedMemberNames =
                                this.cachedFieldNames.Concat(this.cachedMethodNames).Concat(this.cachedPropertyNames).
                                    ExcludeIndices().Distinct().ToArray();
                        }

                        return this.cachedMemberNames;
                    });
        }

        object IDynamic.Invoke(object[] args, bool asConstructor)
        {
            return this.thisExpando.InvokeMember(
                SpecialMemberNames.Default,
                asConstructor ? BindingFlags.CreateInstance : BindingFlags.InvokeMethod,
                null,
                this.thisExpando,
                args,
                null,
                CultureInfo.InvariantCulture,
                null);
        }

        object IDynamic.InvokeMethod(string name, object[] args)
        {
            return this.thisExpando.InvokeMember(
                name, BindingFlags.InvokeMethod, null, this.thisExpando, args, null, CultureInfo.InvariantCulture, null);
        }

        void IDynamic.SetProperty(string name, object value)
        {
            this.thisExpando.InvokeMember(
                name,
                BindingFlags.SetProperty,
                null,
                this.thisExpando,
                new[] { value },
                null,
                CultureInfo.InvariantCulture,
                null);
        }

        void IDynamic.SetProperty(int index, object value)
        {
            this.thisDynamic.SetProperty(index.ToString(CultureInfo.InvariantCulture), value);
        }

        FieldInfo IExpando.AddField(string name)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        if ((this.targetDynamic != null)
                            || ((this.targetPropertyBag != null) && !this.targetPropertyBag.IsReadOnly)
                            || (this.targetDynamicMetaObject != null))
                        {
                            this.AddExpandoMemberName(name);
                            return fieldMap.GetMember(name);
                        }

                        throw new NotSupportedException("Object does not support dynamic fields");
                    });
        }

        MethodInfo IExpando.AddMethod(string name, Delegate method)
        {
            throw new NotImplementedException();
        }

        PropertyInfo IExpando.AddProperty(string name)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        if ((this.targetDynamic != null)
                            || ((this.targetPropertyBag != null) && !this.targetPropertyBag.IsReadOnly)
                            || (this.targetDynamicMetaObject != null))
                        {
                            this.AddExpandoMemberName(name);
                            return propertyMap.GetMember(name);
                        }

                        throw new NotSupportedException("Object does not support dynamic properties");
                    });
        }

        FieldInfo IReflect.GetField(string name, BindingFlags bindFlags)
        {
            FieldInfo[] fields = this.thisExpando.GetFields(bindFlags).Where(field => field.Name == name).ToArray();
            if (fields.Length < 1)
            {
                return null;
            }

            if (fields.Length > 1)
            {
                throw new AmbiguousMatchException(
                    MiscHelpers.FormatInvariant("Object has multiple fields named '{0}'", name));
            }

            return fields[0];
        }

        FieldInfo[] IReflect.GetFields(BindingFlags bindFlags)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        // ReSharper disable CoVariantArrayConversion

                        bool updated;
                        this.UpdateFieldNames(out updated);
                        if (updated || (this.cachedFields == null))
                        {
                            this.cachedFields = fieldMap.GetMembers(this.cachedFieldNames);
                        }

                        return this.cachedFields;

                        // ReSharper restore CoVariantArrayConversion
                    });
        }

        MemberInfo[] IReflect.GetMember(string name, BindingFlags bindFlags)
        {
            return this.thisExpando.GetMembers(bindFlags).Where(member => member.Name == name).ToArray();
        }

        MemberInfo[] IReflect.GetMembers(BindingFlags bindFlags)
        {
            return
                this.thisExpando.GetFields(bindFlags).Cast<MemberInfo>().Concat(this.thisExpando.GetMethods(bindFlags)).
                    Concat(this.thisExpando.GetProperties(bindFlags)).ToArray();
        }

        MethodInfo IReflect.GetMethod(string name, BindingFlags bindFlags)
        {
            MethodInfo[] methods = this.thisExpando.GetMethods(bindFlags).Where(method => method.Name == name).ToArray();
            if (methods.Length < 1)
            {
                return null;
            }

            if (methods.Length > 1)
            {
                throw new AmbiguousMatchException(
                    MiscHelpers.FormatInvariant("Object has multiple methods named '{0}'", name));
            }

            return methods[0];
        }

        MethodInfo IReflect.GetMethod(
            string name, BindingFlags bindFlags, Binder binder, Type[] types, ParameterModifier[] modifiers)
        {
            throw new NotImplementedException();
        }

        MethodInfo[] IReflect.GetMethods(BindingFlags bindFlags)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        // ReSharper disable CoVariantArrayConversion

                        bool updated;
                        this.UpdateMethodNames(out updated);
                        if (updated || (this.cachedMethods == null))
                        {
                            this.cachedMethods = methodMap.GetMembers(this.cachedMethodNames);
                        }

                        return this.cachedMethods;

                        // ReSharper restore CoVariantArrayConversion
                    });
        }

        PropertyInfo[] IReflect.GetProperties(BindingFlags bindFlags)
        {
            return this.engine.HostInvoke(
                () =>
                    {
                        // ReSharper disable CoVariantArrayConversion

                        bool updated;
                        this.UpdatePropertyNames(out updated);
                        if (updated || (this.cachedProperties == null))
                        {
                            this.cachedProperties = propertyMap.GetMembers(this.cachedPropertyNames);
                        }

                        return this.cachedProperties;

                        // ReSharper restore CoVariantArrayConversion
                    });
        }

        PropertyInfo IReflect.GetProperty(
            string name,
            BindingFlags bindFlags,
            Binder binder,
            Type returnType,
            Type[] types,
            ParameterModifier[] modifiers)
        {
            throw new NotImplementedException();
        }

        PropertyInfo IReflect.GetProperty(string name, BindingFlags bindFlags)
        {
            PropertyInfo[] properties =
                this.thisExpando.GetProperties(bindFlags).Where(property => property.Name == name).ToArray();
            if (properties.Length < 1)
            {
                return null;
            }

            if (properties.Length > 1)
            {
                throw new AmbiguousMatchException(
                    MiscHelpers.FormatInvariant("Object has multiple properties named '{0}'", name));
            }

            return properties[0];
        }

        object IReflect.InvokeMember(
            string name,
            BindingFlags invokeFlags,
            Binder binder,
            object invokeTarget,
            object[] wrappedArgs,
            ParameterModifier[] modifiers,
            CultureInfo culture,
            string[] namedParams)
        {
            return this.engine.MarshalToScript(
                this.engine.HostInvoke(
                    () =>
                        {
                            object[] args = this.engine.MarshalToHost(wrappedArgs, false);

                            bool skipFirst = false;
                            if ((namedParams != null) && (namedParams.Length > 0)
                                && (namedParams[0] == SpecialParamNames.This))
                            {
                                args = args.Skip(1).ToArray();
                                skipFirst = true;
                            }

                            object[] bindArgs = args;
                            if (invokeFlags.HasFlag(BindingFlags.InvokeMethod)
                                || invokeFlags.HasFlag(BindingFlags.CreateInstance))
                            {
                                bindArgs = this.engine.MarshalToHost(wrappedArgs, true);
                                if (skipFirst)
                                {
                                    bindArgs = bindArgs.Skip(1).ToArray();
                                }
                            }

                            return this.InvokeMember(name, invokeFlags, args, bindArgs, culture);
                        }));
        }

        void IExpando.RemoveMember(MemberInfo member)
        {
            this.engine.HostInvoke(
                () =>
                    {
                        if (this.targetDynamic != null)
                        {
                            int index;
                            if (int.TryParse(member.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out index))
                            {
                                if (this.targetDynamic.DeleteProperty(index))
                                {
                                    this.RemoveExpandoMemberName(index.ToString(CultureInfo.InvariantCulture));
                                }
                            }
                            else if (this.targetDynamic.DeleteProperty(member.Name))
                            {
                                this.RemoveExpandoMemberName(member.Name);
                            }
                        }
                        else if (this.targetPropertyBag != null)
                        {
                            if (this.targetPropertyBag.Remove(member.Name))
                            {
                                this.RemoveExpandoMemberName(member.Name);
                            }
                        }
                        else if (this.targetDynamicMetaObject != null)
                        {
                            bool result;
                            if (this.targetDynamicMetaObject.TryDeleteMember(member.Name, out result) && result)
                            {
                                this.RemoveExpandoMemberName(member.Name);
                            }
                        }
                        else
                        {
                            throw new NotSupportedException("Object does not support dynamic members");
                        }
                    });
        }

        #endregion

        #region Methods

        private static object BindOrCreate(ScriptEngine engine, object target, Type type, HostItemFlags flags)
        {
            return BindOrCreate(engine, HostObject.Wrap(target, type), flags);
        }

        private static object BindOrCreate(ScriptEngine engine, HostTarget target, HostItemFlags flags)
        {
            return engine.GetOrCreateHostItem(target, flags, Create);
        }

        private static HostItem Create(ScriptEngine engine, HostTarget target, HostItemFlags flags)
        {
            return new HostItem(engine, target, flags);
        }

        private static object Wrap(ScriptEngine engine, object obj, Type type, HostItemFlags flags)
        {
            Debug.Assert(!(obj is HostItem));

            if (obj == null)
            {
                return null;
            }

            var hostTarget = obj as HostTarget;
            if (hostTarget != null)
            {
                return BindOrCreate(engine, hostTarget, flags);
            }

            if (type == null)
            {
                type = obj.GetType();
            }
            else
            {
                Debug.Assert(type.IsInstanceOfType(obj));
            }

            if (obj is Enum)
            {
                return BindOrCreate(engine, obj, type, flags);
            }

            TypeCode typeCode = Type.GetTypeCode(type);
            if ((typeCode == TypeCode.Object) || (typeCode == TypeCode.DateTime))
            {
                return BindOrCreate(engine, obj, type, flags);
            }

            return obj;
        }

        private void AddExpandoMemberName(string name)
        {
            if (this.expandoMemberNames == null)
            {
                this.expandoMemberNames = new HashSet<string>();
            }

            this.expandoMemberNames.Add(name);
        }

        private void AdjustInvokeFlags(ref BindingFlags invokeFlags)
        {
            const BindingFlags onFlags = BindingFlags.Public | BindingFlags.OptionalParamBinding;

            const BindingFlags offFlags =
                BindingFlags.DeclaredOnly | BindingFlags.IgnoreCase | BindingFlags.ExactBinding;

            const BindingFlags setPropertyFlags =
                BindingFlags.SetProperty | BindingFlags.PutDispProperty | BindingFlags.PutRefDispProperty;

            invokeFlags |= onFlags;
            invokeFlags &= ~offFlags;

            if (this.flags.HasFlag(HostItemFlags.PrivateAccess) || (this.target.Type == this.engine.AccessContext))
            {
                invokeFlags |= BindingFlags.NonPublic;
            }

            if (this.target.Flags.HasFlag(HostTargetFlags.AllowStaticMembers))
            {
                invokeFlags |= BindingFlags.Static;
            }
            else
            {
                invokeFlags &= ~BindingFlags.Static;
            }

            if (this.target.Flags.HasFlag(HostTargetFlags.AllowInstanceMembers))
            {
                invokeFlags |= BindingFlags.Instance;
            }
            else
            {
                invokeFlags &= ~BindingFlags.Instance;
            }

            if (invokeFlags.HasFlag(BindingFlags.GetProperty))
            {
                invokeFlags |= BindingFlags.GetField;
            }

            if ((invokeFlags & setPropertyFlags) != 0)
            {
                invokeFlags |= BindingFlags.SetField;
            }
        }

        private void BindSpecialTarget()
        {
            if (this.BindSpecialTarget(out this.targetDynamic))
            {
                this.targetPropertyBag = null;
                this.targetList = null;
                this.targetDynamicMetaObject = null;
            }
            else if (this.BindSpecialTarget(out this.targetPropertyBag))
            {
                this.targetList = null;
                this.targetDynamicMetaObject = null;
            }
            else
            {
                IDynamicMetaObjectProvider dynamicMetaObjectProvider;
                if (!this.flags.HasFlag(HostItemFlags.HideDynamicMembers)
                    && this.BindSpecialTarget(out dynamicMetaObjectProvider))
                {
                    this.targetDynamicMetaObject =
                        dynamicMetaObjectProvider.GetMetaObject(Expression.Constant(this.target.InvokeTarget));
                    this.targetList = null;
                }
                else
                {
                    this.targetDynamicMetaObject = null;
                    this.BindSpecialTarget(out this.targetList);
                }
            }
        }

        private bool BindSpecialTarget<T>(out T specialTarget) where T : class
        {
            // The check here is required because the item may be bound to a specific target base
            // class or interface - one that must not trigger special treatment.

            if (typeof(T).IsAssignableFrom(this.target.Type))
            {
                specialTarget = this.target.InvokeTarget as T;
                return specialTarget != null;
            }

            specialTarget = null;
            return false;
        }

        private string[] GetAllFieldNames()
        {
            if ((this.targetDynamic == null) && (this.targetPropertyBag == null))
            {
                return this.GetLocalFieldNames().Concat(this.GetLocalEventNames()).Distinct().ToArray();
            }

            return MiscHelpers.GetEmptyArray<string>();
        }

        private string[] GetAllMethodNames()
        {
            IEnumerable<string> names = this.target.GetAuxMethodNames(this.GetMethodBindFlags()).AsEnumerable();
            if ((this.targetDynamic == null) && (this.targetPropertyBag == null))
            {
                names = names.Concat(this.GetLocalMethodNames());
                if (this.target.Flags.HasFlag(HostTargetFlags.AllowExtensionMethods))
                {
                    this.cachedExtensionMethodSummary = this.engine.ExtensionMethodSummary;
                    names = names.Concat(this.cachedExtensionMethodSummary.MethodNames);
                }
            }

            return names.Distinct().ToArray();
        }

        private string[] GetAllPropertyNames()
        {
            IEnumerable<string> names = this.target.GetAuxPropertyNames(this.GetCommonBindFlags()).AsEnumerable();
            if (this.targetDynamic != null)
            {
                names = names.Concat(this.targetDynamic.GetPropertyNames());
                names =
                    names.Concat(
                        this.targetDynamic.GetPropertyIndices().Select(
                            index => index.ToString(CultureInfo.InvariantCulture)));
            }
            else if (this.targetPropertyBag != null)
            {
                names = names.Concat(this.targetPropertyBag.Keys);
            }
            else
            {
                names = names.Concat(this.GetLocalPropertyNames());

                if (this.targetList != null)
                {
                    this.cachedListCount = this.targetList.Count;
                    if (this.cachedListCount > 0)
                    {
                        names =
                            names.Concat(
                                Enumerable.Range(0, this.cachedListCount).Select(
                                    index => index.ToString(CultureInfo.InvariantCulture)));
                    }
                }

                if (this.targetDynamicMetaObject != null)
                {
                    names = names.Concat(this.targetDynamicMetaObject.GetDynamicMemberNames());
                }
            }

            if (this.expandoMemberNames != null)
            {
                names = names.Except(this.expandoMemberNames);
            }

            return names.Distinct().ToArray();
        }

        private BindingFlags GetCommonBindFlags()
        {
            var bindFlags = BindingFlags.Public;

            if (this.flags.HasFlag(HostItemFlags.PrivateAccess) || (this.target.Type == this.engine.AccessContext))
            {
                bindFlags |= BindingFlags.NonPublic;
            }

            if (this.target.Flags.HasFlag(HostTargetFlags.AllowStaticMembers))
            {
                bindFlags |= BindingFlags.Static;
            }

            if (this.target.Flags.HasFlag(HostTargetFlags.AllowInstanceMembers))
            {
                bindFlags |= BindingFlags.Instance;
            }

            return bindFlags;
        }

        private object GetHostProperty(
            string name, BindingFlags invokeFlags, object[] args, object[] bindArgs, CultureInfo culture)
        {
            if ((this.targetDynamicMetaObject != null)
                && (this.targetDynamicMetaObject.GetDynamicMemberNames().Contains(name)))
            {
                object result;
                if (this.targetDynamicMetaObject.TryGetMember(name, out result))
                {
                    return result;
                }

                if (this.hostMethodMap == null)
                {
                    this.hostMethodMap = new Dictionary<string, HostMethod>();
                }

                HostMethod hostMethod;
                if (!this.hostMethodMap.TryGetValue(name, out hostMethod))
                {
                    hostMethod = new HostMethod(this, name);
                    this.hostMethodMap.Add(name, hostMethod);
                }

                return hostMethod;
            }

            PropertyInfo property = this.target.Type.GetScriptableProperty(name, invokeFlags, bindArgs);
            if (property != null)
            {
                object result = property.GetValue(
                    this.target.InvokeTarget, invokeFlags, Type.DefaultBinder, args, culture);
                return property.IsRestrictedForScript() ? HostObject.WrapResult(result, property.PropertyType) : result;
            }

            if (this.target.Type.GetScriptableProperties(name, invokeFlags).Any())
            {
                if (this.hostIndexedPropertyMap == null)
                {
                    this.hostIndexedPropertyMap = new Dictionary<string, HostIndexedProperty>();
                }

                HostIndexedProperty hostIndexedProperty;
                if (!this.hostIndexedPropertyMap.TryGetValue(name, out hostIndexedProperty))
                {
                    hostIndexedProperty = new HostIndexedProperty(this, name);
                    this.hostIndexedPropertyMap.Add(name, hostIndexedProperty);
                }

                return hostIndexedProperty;
            }

            FieldInfo field = this.target.Type.GetScriptableField(name, invokeFlags);
            if (field != null)
            {
                object result = field.GetValue(this.target.InvokeTarget);
                return field.IsRestrictedForScript() ? HostObject.WrapResult(result, field.FieldType) : result;
            }

            EventInfo eventInfo = this.target.Type.GetScriptableEvent(name, invokeFlags);
            if (eventInfo != null)
            {
                Type type = typeof(EventSource<>).MakeSpecificType(eventInfo.EventHandlerType);
                return type.CreateInstance(BindingFlags.NonPublic, this.engine, this.target.InvokeTarget, eventInfo);
            }

            MethodInfo method =
                this.thisExpando.GetMethods(this.GetMethodBindFlags()).FirstOrDefault(
                    testMethod => testMethod.Name == name);
            if (method != null)
            {
                if (this.hostMethodMap == null)
                {
                    this.hostMethodMap = new Dictionary<string, HostMethod>();
                }

                HostMethod hostMethod;
                if (!this.hostMethodMap.TryGetValue(name, out hostMethod))
                {
                    hostMethod = new HostMethod(this, name);
                    this.hostMethodMap.Add(name, hostMethod);
                }

                return hostMethod;
            }

            return Nonexistent.Value;
        }

        private string[] GetLocalEventNames()
        {
            if (this.cachedLocalEventNames == null)
            {
                IEnumerable<EventInfo> localEvents = this.target.Type.GetScriptableEvents(this.GetCommonBindFlags());
                this.cachedLocalEventNames = localEvents.Select(eventInfo => eventInfo.GetScriptName()).ToArray();
            }

            return this.cachedLocalEventNames;
        }

        private string[] GetLocalFieldNames()
        {
            if (this.cachedLocalFieldNames == null)
            {
                IEnumerable<FieldInfo> localFields = this.target.Type.GetScriptableFields(this.GetCommonBindFlags());
                this.cachedLocalFieldNames = localFields.Select(field => field.GetScriptName()).ToArray();
            }

            return this.cachedLocalFieldNames;
        }

        private string[] GetLocalMethodNames()
        {
            if (this.cachedLocalMethodNames == null)
            {
                IEnumerable<MethodInfo> localMethods = this.target.Type.GetScriptableMethods(this.GetMethodBindFlags());
                this.cachedLocalMethodNames = localMethods.Select(method => method.GetScriptName()).ToArray();
            }

            return this.cachedLocalMethodNames;
        }

        private string[] GetLocalPropertyNames()
        {
            if (this.cachedLocalPropertyNames == null)
            {
                IEnumerable<PropertyInfo> localProperties =
                    this.target.Type.GetScriptableProperties(this.GetCommonBindFlags());
                this.cachedLocalPropertyNames = localProperties.Select(property => property.GetScriptName()).ToArray();
            }

            return this.cachedLocalPropertyNames;
        }

        private BindingFlags GetMethodBindFlags()
        {
            return this.GetCommonBindFlags() | BindingFlags.OptionalParamBinding;
        }

        private void Initialize()
        {
            if ((this.target is HostObject) || (this.target is IHostVariable) || (this.target is IByRefArg))
            {
                this.BindSpecialTarget();
            }

            if (this.flags.HasFlag(HostItemFlags.PrivateAccess))
            {
                this.accessContext = this.target.Type;
            }

            var scriptableObject = this.target.Target as IScriptableObject;
            if (scriptableObject != null)
            {
                scriptableObject.OnExposedToScriptCode(this.engine);
            }
        }

        private object InvokeDynamicMember(string name, BindingFlags invokeFlags, object[] args)
        {
            if (invokeFlags.HasFlag(BindingFlags.CreateInstance))
            {
                if (name == SpecialMemberNames.Default)
                {
                    return this.targetDynamic.Invoke(args, true);
                }

                throw new InvalidOperationException("Invalid constructor invocation");
            }

            if (invokeFlags.HasFlag(BindingFlags.InvokeMethod))
            {
                if (name == SpecialMemberNames.Default)
                {
                    try
                    {
                        return this.targetDynamic.Invoke(args, false);
                    }
                    catch (Exception)
                    {
                        if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                        {
                            return this.targetDynamic;
                        }

                        throw;
                    }
                }

                try
                {
                    return this.targetDynamic.InvokeMethod(name, args);
                }
                catch (Exception)
                {
                    if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                    {
                        return this.targetDynamic.GetProperty(name);
                    }

                    throw;
                }
            }

            if (invokeFlags.HasFlag(BindingFlags.GetField))
            {
                return this.targetDynamic.GetProperty(name);
            }

            if (invokeFlags.HasFlag(BindingFlags.SetField))
            {
                if (args.Length == 1)
                {
                    object value = args[0];
                    this.targetDynamic.SetProperty(name, value);
                    return value;
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            throw new InvalidOperationException("Invalid member invocation mode");
        }

        private object InvokeHostMember(
            string name, BindingFlags invokeFlags, object[] args, object[] bindArgs, CultureInfo culture)
        {
            if (invokeFlags.HasFlag(BindingFlags.CreateInstance))
            {
                if (name == SpecialMemberNames.Default)
                {
                    var hostType = this.target as HostType;
                    if (hostType != null)
                    {
                        HostType[] typeArgs = GetTypeArgs(args).Select(HostType.Wrap).ToArray();
                        if (typeArgs.Length > 0)
                        {
                            // ReSharper disable CoVariantArrayConversion

                            object result;
                            if (hostType.TryInvoke(BindingFlags.InvokeMethod, typeArgs, typeArgs, out result))
                            {
                                hostType = result as HostType;
                                if (hostType != null)
                                {
                                    args = args.Skip(typeArgs.Length).ToArray();

                                    Type specificType = hostType.GetSpecificType();
                                    if (typeof(Delegate).IsAssignableFrom(specificType))
                                    {
                                        if (args.Length != 1)
                                        {
                                            throw new InvalidOperationException("Invalid constructor invocation");
                                        }

                                        return DelegateFactory.CreateDelegate(this.engine, args[0], specificType);
                                    }

                                    return specificType.CreateInstance(invokeFlags, args);
                                }
                            }

                            throw new InvalidOperationException("Invalid constructor invocation");

                            // ReSharper restore CoVariantArrayConversion
                        }

                        Type type = hostType.GetSpecificType();
                        if (typeof(Delegate).IsAssignableFrom(type))
                        {
                            if (args.Length != 1)
                            {
                                throw new InvalidOperationException("Invalid constructor invocation");
                            }

                            return DelegateFactory.CreateDelegate(this.engine, args[0], type);
                        }

                        return type.CreateInstance(invokeFlags, args);
                    }

                    if (this.targetDynamicMetaObject != null)
                    {
                        object result;
                        if (this.targetDynamicMetaObject.TryCreateInstance(args, out result))
                        {
                            return result;
                        }
                    }
                }

                throw new InvalidOperationException("Invalid constructor invocation");
            }

            if (invokeFlags.HasFlag(BindingFlags.InvokeMethod))
            {
                if (name == SpecialMemberNames.Default)
                {
                    object result;
                    if (InvokeHelpers.TryInvokeObject(
                        this.target, invokeFlags, args, bindArgs, this.targetDynamicMetaObject != null, out result))
                    {
                        return result;
                    }

                    if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                    {
                        return this.target;
                    }

                    throw new NotSupportedException("Object does not support the requested invocation operation");
                }

                if ((this.targetDynamicMetaObject != null)
                    && (this.targetDynamicMetaObject.GetDynamicMemberNames().Contains(name)))
                {
                    object result;
                    if (this.targetDynamicMetaObject.TryInvokeMember(name, invokeFlags, args, out result))
                    {
                        return result;
                    }
                }

                if (this.thisExpando.GetMethods(this.GetMethodBindFlags()).Any(method => method.Name == name))
                {
                    return this.InvokeMethod(name, args, bindArgs);
                }

                PropertyInfo property = this.target.Type.GetScriptableProperty(
                    name, this.GetCommonBindFlags(), MiscHelpers.GetEmptyArray<object>());
                if ((property != null) && (typeof(Delegate).IsAssignableFrom(property.PropertyType)))
                {
                    var del =
                        (Delegate)
                        property.GetValue(
                            this.target.InvokeTarget,
                            invokeFlags | BindingFlags.GetProperty,
                            Type.DefaultBinder,
                            MiscHelpers.GetEmptyArray<object>(),
                            culture);
                    return InvokeHelpers.InvokeDelegate(del, args);
                }

                FieldInfo field = this.target.Type.GetScriptableField(name, this.GetCommonBindFlags());
                if ((field != null) && (typeof(Delegate).IsAssignableFrom(field.FieldType)))
                {
                    var del = (Delegate)field.GetValue(this.target.InvokeTarget);
                    return InvokeHelpers.InvokeDelegate(del, args);
                }

                if (invokeFlags.HasFlag(BindingFlags.GetField))
                {
                    return this.GetHostProperty(name, invokeFlags, args, bindArgs, culture);
                }

                throw new MissingMemberException(
                    MiscHelpers.FormatInvariant("Object has no suitable method named '{0}'", name));
            }

            if (invokeFlags.HasFlag(BindingFlags.GetField))
            {
                return this.GetHostProperty(name, invokeFlags, args, bindArgs, culture);
            }

            if (invokeFlags.HasFlag(BindingFlags.SetField))
            {
                return this.SetHostProperty(name, invokeFlags, args, bindArgs, culture);
            }

            throw new InvalidOperationException("Invalid member invocation mode");
        }

        private object InvokeListElement(int index, BindingFlags invokeFlags, object[] args, object[] bindArgs)
        {
            if (invokeFlags.HasFlag(BindingFlags.InvokeMethod))
            {
                object result;
                if (InvokeHelpers.TryInvokeObject(this.targetList[index], invokeFlags, args, bindArgs, true, out result))
                {
                    return result;
                }

                if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                {
                    return this.targetList[index];
                }

                throw new NotSupportedException("Object does not support the requested invocation operation");
            }

            if (invokeFlags.HasFlag(BindingFlags.GetField))
            {
                if (args.Length < 1)
                {
                    return this.targetList[index];
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            if (invokeFlags.HasFlag(BindingFlags.SetField))
            {
                if (args.Length == 1)
                {
                    return this.targetList[index] = args[0];
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            throw new InvalidOperationException("Invalid member invocation mode");
        }

        private object InvokePropertyBagMember(string name, BindingFlags invokeFlags, object[] args, object[] bindArgs)
        {
            if (invokeFlags.HasFlag(BindingFlags.InvokeMethod))
            {
                if (name == SpecialMemberNames.Default)
                {
                    if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                    {
                        return this.targetPropertyBag;
                    }

                    throw new NotSupportedException("Object does not support invocation");
                }

                object value;
                if (!this.targetPropertyBag.TryGetValue(name, out value))
                {
                    throw new MissingMemberException(
                        MiscHelpers.FormatInvariant("Object has no property named '{0}'", name));
                }

                object result;
                if (InvokeHelpers.TryInvokeObject(value, invokeFlags, args, bindArgs, true, out result))
                {
                    return result;
                }

                if (invokeFlags.HasFlag(BindingFlags.GetField) && (args.Length < 1))
                {
                    return value;
                }

                throw new NotSupportedException("Object does not support the requested invocation operation");
            }

            if (invokeFlags.HasFlag(BindingFlags.GetField))
            {
                if (args.Length < 1)
                {
                    object value;
                    return this.targetPropertyBag.TryGetValue(name, out value) ? value : Nonexistent.Value;
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            if (invokeFlags.HasFlag(BindingFlags.SetField))
            {
                if (args.Length == 1)
                {
                    return this.targetPropertyBag[name] = args[0];
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            throw new InvalidOperationException("Invalid member invocation mode");
        }

        private void RemoveExpandoMemberName(string name)
        {
            if (this.expandoMemberNames != null)
            {
                this.expandoMemberNames.Remove(name);
            }
        }

        private object SetHostProperty(
            string name, BindingFlags invokeFlags, object[] args, object[] bindArgs, CultureInfo culture)
        {
            if (name == SpecialMemberNames.Default)
            {
                // special case to enable JScript/VBScript "x(a) = b" syntax when x is a host indexed property 

                object result;
                if (InvokeHelpers.TryInvokeObject(this.target, invokeFlags, args, bindArgs, false, out result))
                {
                    return result;
                }

                throw new InvalidOperationException("Invalid property assignment");
            }

            if ((this.targetDynamicMetaObject != null) && (args.Length == 1))
            {
                object result;
                if (this.targetDynamicMetaObject.TrySetMember(name, args[0], out result))
                {
                    return result;
                }
            }

            if (args.Length < 1)
            {
                throw new InvalidOperationException("Invalid argument count");
            }

            PropertyInfo property = this.target.Type.GetScriptableProperty(
                name, invokeFlags, bindArgs.Take(bindArgs.Length - 1).ToArray());
            if (property != null)
            {
                if (property.IsReadOnlyForScript())
                {
                    throw new UnauthorizedAccessException("Property is read-only");
                }

                object value = args[args.Length - 1];
                if (property.PropertyType.IsAssignableFrom(ref value))
                {
                    property.SetValue(
                        this.target.InvokeTarget,
                        value,
                        invokeFlags,
                        Type.DefaultBinder,
                        args.Take(args.Length - 1).ToArray(),
                        culture);
                    return value;
                }

                throw new ArgumentException("Invalid property assignment");
            }

            FieldInfo field = this.target.Type.GetScriptableField(name, invokeFlags);
            if (field != null)
            {
                if (field.IsReadOnlyForScript())
                {
                    throw new UnauthorizedAccessException("Field is read-only");
                }

                if (args.Length == 1)
                {
                    object value = args[0];
                    if (field.FieldType.IsAssignableFrom(ref value))
                    {
                        field.SetValue(this.target.InvokeTarget, value);
                        return value;
                    }

                    throw new ArgumentException("Invalid field assignment");
                }

                throw new InvalidOperationException("Invalid argument count");
            }

            throw new MissingMemberException(
                MiscHelpers.FormatInvariant("Object has no suitable property or field named '{0}'", name));
        }

        private void UpdateFieldNames(out bool updated)
        {
            if (this.cachedFieldNames == null)
            {
                this.cachedFieldNames = this.GetAllFieldNames();
                updated = true;
            }
            else
            {
                updated = false;
            }
        }

        private void UpdateMethodNames(out bool updated)
        {
            if ((this.cachedMethodNames == null)
                ||
                (this.target.Flags.HasFlag(HostTargetFlags.AllowExtensionMethods)
                 && (this.cachedExtensionMethodSummary != this.engine.ExtensionMethodSummary)))
            {
                this.cachedMethodNames = this.GetAllMethodNames();
                updated = true;
            }
            else
            {
                updated = false;
            }
        }

        private void UpdatePropertyNames(out bool updated)
        {
            if ((this.cachedPropertyNames == null) || (this.targetDynamic != null) || (this.targetPropertyBag != null)
                || (this.targetDynamicMetaObject != null)
                || ((this.targetList != null) && (this.cachedListCount != this.targetList.Count)))
            {
                this.cachedPropertyNames = this.GetAllPropertyNames();
                updated = true;
            }
            else
            {
                updated = false;
            }
        }

        #endregion
    }
}